package com.mspbots.core.teams.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.mspbots.common.StringKit;
import com.mspbots.common.TimeZoneUtils;
import com.mspbots.core.AbstractBaseServiceImpl;
import com.mspbots.core.RestMessageObject;
import com.mspbots.core.attendance.Attendance;
import com.mspbots.core.attendance.AttendancesController;
import com.mspbots.core.attendance.AttendancesService;
import com.mspbots.core.config.PropertiesConfiguration;
import com.mspbots.core.enums.AttendanceTypeEnum;
import com.mspbots.core.teams.TeamsUserMessageController;
import com.mspbots.core.teams.mapper.TeamsUserMessageMapper;
import com.mspbots.core.teams.model.CheckBeforeOut;
import com.mspbots.core.teams.model.TeamsUser;
import com.mspbots.core.teams.model.TeamsUserMessage;
import com.mspbots.core.teams.model.TenantAttendance;
import com.mspbots.core.teams.service.TeamsUserMessageService;
import com.mspbots.core.teams.service.TeamsUserService;
import com.mspbots.core.ticketai.model.TicketAuditor;
import com.mspbots.core.ticketai.service.TicketService;
import com.mspbots.core.wise.controller.params.UserMappingParams;
import com.mspbots.core.wise.mapper.UsersMapper;
import com.mspbots.core.wise.model.*;
import com.mspbots.core.wise.model.dto.UserMappingResponse;
import com.mspbots.core.wise.service.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * @author devide he
 */
@Log4j2
@Service
public class TeamsUserMessageServiceImpl extends AbstractBaseServiceImpl<TeamsUserMessageMapper, TeamsUserMessage>
        implements TeamsUserMessageService {

    private static final String KEYWORD_WISE_SITE = "mspbots.sync.wise.site";
    private static String MESSAGE_PARAM_STYLE = "<span style='color:#FF0000;font-weight:bold'>{}</span>";
    private final TenantService tenantService;
    private final TeamsUserService teamsUserService;
    private final TenantUserMappingService tenantUserMappingService;
    private final AttendancesService attendancesService;
    private final TenantTimeEntryService tenantTimeEntryService;
    private final TenantUserService tenantUserService;
    private final TicketService ticketService;
    private final TenantConfigurationService tenantConfigurationService;
    private final PropertiesConfiguration.CoreProperties coreProperties;
    private final RestTemplate restTemplate;
    @Autowired
    private UsersMapper usersMapper;

    public TeamsUserMessageServiceImpl(TenantService tenantService,
                                       TeamsUserService teamsUserService,
                                       AttendancesService attendancesService,
                                       TenantUserMappingService tenantUserMappingService,
                                       TenantTimeEntryService tenantTimeEntryService,
                                       TenantUserService tenantUserService,
                                       TicketService ticketService,
                                       TenantConfigurationService tenantConfigurationService,
                                       PropertiesConfiguration.CoreProperties coreProperties, RestTemplate restTemplate) {
        this.tenantService = tenantService;
        this.teamsUserService = teamsUserService;
        this.attendancesService = attendancesService;
        this.tenantUserMappingService = tenantUserMappingService;
        this.tenantTimeEntryService = tenantTimeEntryService;
        this.tenantUserService = tenantUserService;
        this.ticketService = ticketService;
        this.tenantConfigurationService = tenantConfigurationService;
        this.coreProperties = coreProperties;
        this.restTemplate = restTemplate;
    }

    /**
     * TableId 注解存在更新记录，否插入一条记录
     *
     * @param entity 实体对象
     * @return boolean
     */
    @Override
    public TeamsUserMessage saveOnUser(TeamsUserMessage entity) {
        TeamsUser teamsUser = this.teamsUserService.getOne(Wrappers.<TeamsUser>lambdaQuery()
                .eq(TeamsUser::getTeamsUserId, entity.getTeamsUserId())
                .orderByDesc(TeamsUser::getCreateDate)
                .last("limit 1"));
        if (!ObjectUtils.isEmpty(teamsUser)) {
            entity.setTenantId(teamsUser.getTenantId());
        } else {
            entity.setTenantId(0L);
        }
        this.saveOrUpdate(entity);
        return entity;
    }

    @Override
    public IPage<TeamsUserMessage> selectPage(Page<TeamsUserMessage> page, TeamsUserMessage queryParams) {
        IPage<TeamsUserMessage> teamsUserMessagePage = this.page(page.addOrder(OrderItem.desc("id")),
                Wrappers.lambdaQuery(queryParams));
        List<TeamsUserMessage> tenantTeamsUserMessages = teamsUserMessagePage.getRecords()
                .parallelStream().peek(userMessage -> {
                    TeamsUser teamsUser = this.teamsUserService.selectByTeamsUserId(userMessage.getTeamsUserId());
                    if (!ObjectUtils.isEmpty(teamsUser)) {
                        userMessage.setTeamsUserName(teamsUser.getTeamsDisplayName());
                    }
                    Tenant tenant = this.tenantService.getById(userMessage.getTenantId());
                    if (!ObjectUtils.isEmpty(teamsUser)) {
                        userMessage.setTenantName(tenant.getShortName());
                    }
                }).collect(Collectors.toList());
        teamsUserMessagePage.setRecords(tenantTeamsUserMessages);
        return teamsUserMessagePage;
    }

    @Override
    public Object messageHandlers(String message, TeamsUserMessageController.SenderMessageBody body) {

        UserMappingResponse mapping = this.tenantUserMappingService
                .findByOne(UserMappingParams.builder().teamsUserId(body.getTeamsUserId()).build());
        if (ObjectUtil.isEmpty(mapping)) {
            return RestMessageObject.ok("Your Teams user is not mapped with a ConnectWise user, Please check your ConnectWise API setting.");
        }

        String result;
        switch (message) {
            case "in":
                result = checkIn(mapping, body);
                break;
            case "out":
                result = checkOut(mapping, body);
                break;
            case "lunch":
                result = lunchIn(mapping, body);
                break;
            case "lunchback":
                result = lunchOut(mapping);
                break;
            case "break":
                result = breakIn(mapping, body);
                break;
            case "breakback":
            case "back":
                result = breakBack(mapping);
                break;
            case "status":
                result = status(mapping);
                break;
            default:
                result = "Unknown command. Please type “help” to get a list of the available command.";
                break;
        }
        return RestMessageObject.ok(result);
    }

    private String checkIn(UserMappingResponse mapping, TeamsUserMessageController.SenderMessageBody body) {

        AttendancesController.AttendanceParams queryLogParams = AttendancesController.AttendanceParams.builder().build();
        queryLogParams.setTenantId(mapping.getTenantId());
        queryLogParams.setTeamsId(mapping.getTeamsUserId());
        queryLogParams.setUserId(mapping.getTenantUserId());
        LocalDateTime startTime = LocalDateTime.now(ZoneOffset.of(mapping.getTz()));
        queryLogParams.setStartTime(startTime.minusHours(12));
        queryLogParams.setEndTime(startTime.plusMinutes(1));
        queryLogParams.setType(AttendanceTypeEnum.IN);
        Attendance attendance = this.attendancesService.findLogLast(queryLogParams);

        if (ObjectUtils.isEmpty(attendance)) {
            if (ObjectUtils.isEmpty(body.getLogTime()) && ObjectUtils.isEmpty(body.getTime())) {
                attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.IN);
            } else {
                if (!ObjectUtils.isEmpty(body.getTime())) {
                    LocalTime time = LocalTime.parse(body.getTime());
                    LocalTime afterTime = LocalTime.now(ZoneOffset.of(mapping.getTz()));
                    if (time.isAfter(afterTime)) {
                        return "You have input an IN time in the future. " +
                                "Please use a past time when you use in " +
                                "at time command. Thanks!";
                    }
                    body.setLogTime(LocalDateTime.of(LocalDate.now(ZoneOffset.of(mapping.getTz())),
                            LocalTime.parse(body.getTime())));
                }
                body.setFromType(body.getFromType());
                attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.IN, body);
            }

            return "Welcome " +
                    StrUtil.format(MESSAGE_PARAM_STYLE, mapping.getUserName()) +
                    ", I have updated your status to " +
                    StrUtil.format(MESSAGE_PARAM_STYLE, "checked in") +
                    ". You started to work on [" +
                    StrUtil.format(MESSAGE_PARAM_STYLE, formatDate(attendance.getLogTime())) +
                    mapping.getTzStr() + "].";
//                    ", You can check the next ticket here: <a href='https://nextticket.itsasap.com/login'>Next Ticket</a>";
        }
        return "You have already checked in on [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
    }

    private String checkOut(UserMappingResponse mapping, TeamsUserMessageController.SenderMessageBody body) {

        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        if (ObjectUtils.isEmpty(attendance)) {
            // return "You haven't check in yet. If you forget clock in, Please use “in at 7:30am format to add the clock in record,then you can type the command again. Thanks!";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        } else if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            return "You haven't ended break yet. Please type ‘back' to end break.";//Leon changes 2019.11.27
        } else if (attendance.getType() == AttendanceTypeEnum.LUNCH) {
            return "You haven't ended lunch yet. Please type ‘back' to end lunch.";//Leon changes 2019.11.27
        }

//        if (!StringUtils.isEmpty(body.getTime())) {
//            LocalTime outTime = LocalTime.parse(body.getTime());
//            LocalTime userCurrentTime = LocalTime.now(ZoneOffset.of(mapping.getTz()));
//            if (outTime.isBefore(userCurrentTime)) {
//                return "You have input an OUT time in the past. " +
//                        "Please use a future time when you use out " +
//                        "at time command. Thanks!";
//            }
//        }

        log.debug("out body : {}", body);
        String result = "";
        if (ObjectUtils.isEmpty(body.getCheck()) || body.getCheck()) {
            TenantUser tenantUser = tenantUserService.getById(mapping.getTenantUserId());
            if (!ObjectUtils.isEmpty(tenantUser)) {
                mapping.setCwUid(tenantUser.getCwUid());
                Map<String, Object> timeEntryCheck = checkWorkingHours(mapping, body.getTime());
                String ticketCheckResult = checkTicket(mapping);
//            String ticketCheckResult = "";

                // out fail
                if (!ObjectUtils.isEmpty(timeEntryCheck.get("utilizationLowerStr"))
                        || !ObjectUtils.isEmpty(timeEntryCheck.get("overlappedStr"))
                        || !StringUtils.isEmpty(ticketCheckResult)) {
                    result = "Dear " + mapping.getFirstName() + ", MSPbots find following issues that will need you to address before you leave:  <br><br>";
                    // check if utilization lower
                    if (!ObjectUtils.isEmpty(timeEntryCheck.get("utilizationLowerStr"))) {
                        result += "&nbsp;&nbsp;&nbsp;" + "● " + timeEntryCheck.get("utilizationLowerStr") + "<br>";
                    }

                    // check if overlapped
                    if (!ObjectUtils.isEmpty(timeEntryCheck.get("overlappedStr"))) {
                        result += "&nbsp;&nbsp;&nbsp;" + "● " + timeEntryCheck.get("overlappedStr") + "<br>";
                    }

                    if (!StringUtils.isEmpty(ticketCheckResult)) {
                        result += "&nbsp;&nbsp;&nbsp;" + "● " + ticketCheckResult + "<br>";
                    }
                    result += "    <br><span style='word-break:normal'>Type \"Out\" again after you fix them. If you need to leave now without fix the issues, type \"Out nocheck\", bot will skip the check and clock you out.</span>";
                    return result;

                } else {
                    result = timeEntryCheck.get("utilizationHigherStr").toString();
                }
            }


        }


        if (ObjectUtils.isEmpty(body.getLogTime()) && ObjectUtils.isEmpty(body.getTime())) {
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.OUT);
        } else {
            if (!ObjectUtils.isEmpty(body.getTime())) {
                body.setLogTime(LocalDateTime.of(LocalDate.now(ZoneId.of(mapping.getTz())), LocalTime.parse(body.getTime())));
            }
            body.setFromType(body.getFromType());
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.OUT, body);
        }
        if (StringUtils.isEmpty(result)) {
            return "Dear " + mapping.getUserName() + ", your end of day check is done, you are checked out at "
                    + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "! See you next day!";
        } else {
            return result + " Your end of day check is done, you are checked out at "
                    + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "! See you next day!";
        }

    }

    @Override
    public CheckBeforeOut checkUtilizationBeforeOut(String teamsUserId, String time) {
        log.debug("teamsUserId: {}", teamsUserId);
        log.debug("time: {}", time);
        CheckBeforeOut check = new CheckBeforeOut();
        UserMappingResponse mapping = this.tenantUserMappingService.findByOne(UserMappingParams.builder().teamsUserId(teamsUserId).build());
        if (ObjectUtil.isEmpty(mapping)) {
            check.setIsMappingUser(false);
            return check;
        } else {
            check.setIsMappingUser(true);
        }

        TenantUser tenantUser = tenantUserService.getById(mapping.getTenantUserId());

        if (!ObjectUtils.isEmpty(tenantUser)) {
            check.setIsTenantUser(true);
//            Map<String, Object> timeEntryDuration = getTimeEntryDuration(mapping);
            Map<String, Object> timeEntryDuration = getTimeEntryDurationTest(mapping);
            log.debug("timeEntryDuration: {}", timeEntryDuration);
            Long timeEntryMins = (long) timeEntryDuration.get("timeEntryDuration");
            String timeEntryHours = String.format("%.1f", (float) timeEntryMins / 60);
            Long attendanceMins = getAttendanceDuration(mapping);
            String attendanceHours = String.format("%.1f", (float) attendanceMins / 60);
            double duration = timeEntryMins.doubleValue() / attendanceMins.doubleValue();
            boolean lessWork = duration < 0.85 || duration > 1;
            String utilization = String.format("%.2f", (duration * 100)) + "%";
            check.setLessWork(lessWork);
            check.setUtilization(utilization);
            check.setAttendanceHours(attendanceHours);
            check.setTimeEntryHours(timeEntryHours);
            check.setIsOverlapped((Boolean) timeEntryDuration.get("overlapped"));
            if ((Boolean) timeEntryDuration.get("overlapped")) {
                check.setOverlappedTimeEntry(timeEntryDuration.get("overlappedTimeEntry").toString());
            }

            String cwsite = getConfiguration(mapping.getTenantId(), KEYWORD_WISE_SITE);
            check.setCwsite(cwsite);
            String ticketIds = checkTicket(mapping);
            if (!StringUtils.isEmpty(ticketIds)) {
                check.setIsTicket(true);
                check.setTicketIds(ticketIds);
            } else {
                check.setIsTicket(false);
            }
        } else {
            check.setIsTenantUser(false);
            return check;
        }

        return check;
    }

    @Override
    public RestMessageObject checkBeforeOut(String teamsUserId, String time) {
        log.debug("teamsUserId: {}", teamsUserId);
        log.debug("time: {}", time);
        UserMappingResponse mapping = this.tenantUserMappingService
                .findByOne(UserMappingParams.builder().teamsUserId(teamsUserId).build());
        log.debug("mapping :{}", mapping);
        if (ObjectUtil.isEmpty(mapping)) {
            log.debug("mapping is empty!");
            return RestMessageObject.ok("");
        }
        String result = "";
        TenantUser tenantUser = tenantUserService.getById(mapping.getTenantUserId());

        if (!ObjectUtils.isEmpty(tenantUser)) {
            log.debug("tenantUser id:{}", tenantUser.getId());
            mapping.setCwUid(tenantUser.getCwUid());
            Map<String, Object> timeEntryCheck = checkWorkingHours(mapping, time);
            log.debug("timeEntryCheck:{}", timeEntryCheck);
            String ticketCheckResult = checkTicket(mapping);
            log.debug("timeEntryCheck:{}", timeEntryCheck);
//            String ticketCheckResult = "";

            // out fail
            if (!ObjectUtils.isEmpty(timeEntryCheck.get("utilizationLowerStr"))
                    || !ObjectUtils.isEmpty(timeEntryCheck.get("utilizationLowerStr"))
                    || !StringUtils.isEmpty(ticketCheckResult)) {
                log.debug("utilizationLowerStr and utilizationLowerStr and  ticketCheckResult");
                result = "Dear " + mapping.getFirstName() + ", MSPbots find following issues that will need you to address before you leave:  <br><br>";
                // check if utilization lower
                if (!ObjectUtils.isEmpty(timeEntryCheck.get("utilizationLowerStr"))) {
                    log.debug("utilizationLowerStr is not empty");
                    result += "&nbsp;&nbsp;&nbsp;" + "● " + timeEntryCheck.get("utilizationLowerStr") + "<br>";
                }

                // check if overlapped
                if (!ObjectUtils.isEmpty(timeEntryCheck.get("overlappedStr"))) {
                    log.debug("overlappedStr is not empty");
                    result += "&nbsp;&nbsp;&nbsp;" + "● " + timeEntryCheck.get("overlappedStr") + "<br>";
                }

                if (!StringUtils.isEmpty(ticketCheckResult)) {
                    log.debug("ticketCheckResult is not empty");
                    result += "&nbsp;&nbsp;&nbsp;" + "● " + ticketCheckResult + "<br>";
                }
                result += "    <br><span style='word-break:normal'>Type \"Out\" again after you fix them. If you need to leave now without fix the issues, type \"Out nocheck\", bot will skip the check and clock you out.</span>";
                return RestMessageObject.ok(result);

            } else {
                result = timeEntryCheck.get("utilizationHigherStr").toString();
            }
        } else {
            log.debug("tenantUser is empty!");
        }


        return RestMessageObject.ok(result);
    }

    public String lunchIn(UserMappingResponse mapping, TeamsUserMessageController.SenderMessageBody body) {

        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        if (ObjectUtils.isEmpty(attendance)) {
            //return "You haven't check in yet. Type 'BOD' to check in.";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        } else if (attendance.getType() == AttendanceTypeEnum.OUT || attendance.getType() == AttendanceTypeEnum.BOTOUT) {
            return "You have already check out at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            return "You haven't ended break yet. Please type 'back' to end break.";
        } else if (attendance.getType() == AttendanceTypeEnum.LUNCH) {
            return "You've already started lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
        }
        AttendancesController.AttendanceParams queryLogParams = AttendancesController.AttendanceParams.builder().build();
        queryLogParams.setTenantId(attendance.getTenantId());
        queryLogParams.setTeamsId(attendance.getTeamsUserId());
        queryLogParams.setUserId(attendance.getTenantUserId());
        LocalDateTime startTime = LocalDateTime.now(ZoneOffset.of(mapping.getTz()));
        queryLogParams.setStartTime(startTime.minusHours(12));
        queryLogParams.setEndTime(startTime.plusMinutes(1));
        queryLogParams.setType(AttendanceTypeEnum.LUNCH);
        attendance = this.attendancesService.findLogLast(queryLogParams);
        if (!ObjectUtils.isEmpty(attendance)) {
            return "You've already started lunch in 12 hours,first lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
        }
        body.setLogTime(LocalDateTime.now(ZoneOffset.of(mapping.getTz())));
        attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.LUNCH, body);
        return "You started lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "], type “back” when you are done.";

    }

    public String lunchOut(UserMappingResponse mapping) {

        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        if (ObjectUtils.isEmpty(attendance)) {
            //return "You haven't check in yet. Type 'BOD' to check in.";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        } else if (attendance.getType() == AttendanceTypeEnum.OUT || attendance.getType() == AttendanceTypeEnum.BOTOUT) {
            return "You have already check out at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            return "You have already break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BACK_FROM_BREAK) {
            return "You have already back from break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BACK_FROM_LUNCH) {
            return "You have already back from lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        }

        attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BACK_FROM_LUNCH);
        return "You ended your lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";

    }

    private String breakIn(UserMappingResponse mapping, TeamsUserMessageController.SenderMessageBody body) {

        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        if (ObjectUtils.isEmpty(attendance)) {
            //return "You haven't check in yet. Type 'BOD' to check in.";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        } else if (attendance.getType() == AttendanceTypeEnum.OUT || attendance.getType() == AttendanceTypeEnum.BOTOUT) {
            return "You have already check out at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            return "You have already break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.LUNCH) {
            return "You haven't ended lunch yet. Please type ‘back' to end lunch.";//Leon changes 2019.11.27
        }
        body.setLogTime(LocalDateTime.now(ZoneOffset.of(mapping.getTz())));
        attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BREAK, body);
        return "You started your break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";

    }

    private String breakBack(UserMappingResponse mapping) {

        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        //error tip msg
        if (ObjectUtils.isEmpty(attendance)) {
            //return "You haven't check in yet. Type 'BOD' to check in.";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        } else if (attendance.getType() == AttendanceTypeEnum.OUT || attendance.getType() == AttendanceTypeEnum.BOTOUT) {
            return "You have already check out at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "].";
        } else if (attendance.getType() == AttendanceTypeEnum.BACK_FROM_LUNCH || attendance.getType() == AttendanceTypeEnum.BACK_FROM_BREAK) {
            return "You haven't started your break/lunch yet. Please type ‘break/lunch' to go for a break/lunch.";//Leon change 20191127
        }
//        else if (attendance.getType() == AttendanceTypeEnum.BACK_FROM_BREAK) {
//            return "You haven't ended lunch yet. Type 'lunch' to start lunch.";
//        }
        else if (attendance.getType() == AttendanceTypeEnum.LUNCH) {
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BACK_FROM_LUNCH);
            return "You ended your lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
        } else if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BACK_FROM_BREAK);
            return "You ended your break  at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";

        }
        return "You ended your break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";

    }

    private String status(UserMappingResponse mapping) {
        Attendance attendance = this.attendancesService.attendanceExists(mapping);
        if (ObjectUtils.isEmpty(attendance)) {
            //return "You haven't check in yet. Type 'BOD' to check in.";
            return "You haven't check in yet. If you forget clock in, please use 'in at 7:30am' format to add the clock in record, then you can type the command again. Thanks!";
        }
        String type = attendance.getType().toString();
        if (attendance.getType() == AttendanceTypeEnum.BACK_FROM_BREAK || attendance.getType() == AttendanceTypeEnum.BACK_FROM_LUNCH) {
            type = "BACK";
        }

        return new StringBuilder().append("Dear ")
                .append(StrUtil.format(MESSAGE_PARAM_STYLE, mapping.getUserName()))
                .append(", You current status is ")
                .append(StrUtil.format(MESSAGE_PARAM_STYLE, type))
                .append(", for detailed attendance record, Please click ")
                .append("<a href='https://")
                .append(coreProperties.getHost())
                .append("/102322/DataStream/AttendenceDataStream?userId=")
                .append(attendance.getTenantUserId())
                .append("&tenantId=")
                .append(attendance.getTenantId())
                .append("'>Attenance Report URL</a>").toString();
    }

    private String back(UserMappingResponse mapping) {
        Attendance attendance = this.attendancesService.attendanceExists(mapping);

        if (ObjectUtils.isEmpty(attendance)) {
            return "You haven't started your break/lunch yet. Please type 'break/lunch' to go for a break/lunch.";//Leon change 2019.11.27
        }

        if (attendance.getType() == AttendanceTypeEnum.LUNCH) {
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BACK_FROM_LUNCH);
            return "You ended your lunch at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
        }

        if (attendance.getType() == AttendanceTypeEnum.BREAK) {
            attendance = this.attendancesService.attendance(mapping, AttendanceTypeEnum.BACK_FROM_BREAK);
            return "You ended your break at [" + formatDate(attendance.getLogTime()) + mapping.getTzStr() + "]";
        }

        return "You haven't started your break/lunch yet. Please type 'break/lunch'  to go for a break/lunch."; //Leon change 2019.11.27
    }

    private String formatDate(LocalDateTime now) {
        return formatParam(now.format(DateTimeFormatter.ofPattern("MM/dd HH:mm")));
    }

    private String formatParam(String param) {
        return StrUtil.format("<span style='color:#FF0000;font-weight:bold'>{}</span> ", param);
    }

    private String checkTicket(UserMappingResponse mapping) {

        String status = this.baseMapper.queryTicketStatus(mapping.getTenantId());
        if (ObjectUtils.isEmpty(status)) {
            status = "progress";
        }

        String finalStatus = status;
        List<Long> ticketIds = this.baseMapper.listAuditors(TicketAuditor.builder().tenantId(mapping.getTenantId())
                .updatedBy(mapping.getIdentifier()).statusName(finalStatus).build())
                .stream()
                .filter(item -> !ObjectUtils.isEmpty(item.getStatusName()) && item.getStatusName().toLowerCase().contains(finalStatus.toLowerCase()))
                .map(TicketAuditor::getTicketId)
                .collect(Collectors.toList());

        StringBuilder resultStr = new StringBuilder();
        if (!CollectionUtils.isEmpty(ticketIds)) {
            resultStr.append(ticketIds.stream().map(ticketId -> ticketId + "").collect(Collectors.joining(",")));
        }

        return resultStr.toString();
    }

    private Map<String, Object> checkWorkingHours(UserMappingResponse mapping, String time) {
        Map<String, Object> timeEntryDuration = getTimeEntryDuration(mapping);
        log.debug("timeEntryDuration: {}", timeEntryDuration);
        Long timeEntryMins = (long) timeEntryDuration.get("timeEntryDuration");
        String timeEntryHours = String.format("%.1f", (float) timeEntryMins / 60);
        Long attendanceMins = getAttendanceDuration(mapping, time);
        String attendanceHours = String.format("%.1f", (float) attendanceMins / 60);
        double duration = timeEntryMins.doubleValue() / attendanceMins.doubleValue();
        boolean lessWork = duration < 0.85;
        log.debug("user {} Working duration {} {} {} {}", mapping.getTenantUserId(), timeEntryMins, attendanceMins, duration, lessWork);
        String utilization = String.format("%.2f", (duration * 100)) + "%";
        if (lessWork) {
            timeEntryDuration.put("utilizationLowerStr", "Your un-overlapped utilized work hour is " + StringKit.color(timeEntryHours, "green") + ", your total work hour is " + StringKit.color(attendanceHours, "green") + "," +
                    " today's utilization is " + StringKit.color(utilization, "red") + ", your number is lower than 85%, please review your calendar " + suffixCalendarLink(mapping.getTenantId()) + " to add the missing time entries.");
        } else {
            timeEntryDuration.put("utilizationHigherStr", "Dear " + mapping.getFirstName() + ", congrats!" + " Your total un-overlapped utilized work hours is " + StringKit.color(timeEntryHours, "green") + " hours, your total work hours is " + StringKit.color(attendanceHours, "green") + " hours." +
                    " Today's utilization is " + StringKit.color(utilization, "green") + ", your number is higher than 85%! Keep up a good work! ");
        }
        if ((Boolean) timeEntryDuration.get("overlapped")) {
            timeEntryDuration.put("overlappedStr", "You have overlapped time entry [" + timeEntryDuration.get("overlappedTimeEntry") + "], please go to " + suffixCalendarLink(mapping.getTenantId()) + " link to fix it.");
        }

        return timeEntryDuration;
    }

    private Map<String, Object> getTimeEntryDuration(UserMappingResponse mapping) {
        Map<String, Object> result = new HashMap<>();
        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(mapping.getTz()));
        LocalDateTime begin = TimeZoneUtils.zoneToUtc(LocalDateTime.of(zonedDateTime.toLocalDate(), LocalTime.MIN), mapping.getTz()).toLocalDateTime();
        LocalDateTime end = TimeZoneUtils.zoneToUtc(LocalDateTime.of(zonedDateTime.toLocalDate(), LocalTime.MAX), mapping.getTz()).toLocalDateTime();
        log.debug("begin and end : {} {} ", begin, end);
        QueryWrapper<TenantTimeEntry> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("tenant_id", mapping.getTenantId());
        queryWrapper.eq("cw_uid", mapping.getCwUid());
        queryWrapper.between("entered_date", begin, end);
        queryWrapper.orderByAsc("begin_date");
        List<TenantTimeEntry> list = this.tenantTimeEntryService.list(queryWrapper).parallelStream().map(item -> {
            item.setBeginDate(item.getBeginDate().withSecond(0));
            item.setEndDate(item.getEndDate().withSecond(0));
            return item;
        }).collect(Collectors.toList());

        long count = 0;
        boolean overlapped = false;
        List<TenantTimeEntry> overlappedTimeEntryList = new ArrayList<>();
        String cwWebSit = getConfiguration(mapping.getTenantId(), KEYWORD_WISE_SITE);
        if (list.size() > 0) {
            log.debug("list size : {}", list.size());
            count = Duration.between(list.get(0).getBeginDate(), list.get(0).getEndDate()).toMinutes();
            LocalDateTime endTime = list.get(0).getEndDate();
            for (int i = 1, j = list.size(); i < j; i++) {
                // overlapped
                if (list.get(i).getBeginDate().isBefore(endTime)) {
                    overlapped = true;
                    overlappedTimeEntryList.add(list.get(i - 1));
                    overlappedTimeEntryList.add(list.get(i));

                    if (!list.get(i).getEndDate().isAfter(endTime)) {
                        continue;
                    }
                    count += Duration.between(list.get(i - 1).getEndDate(), list.get(i).getEndDate()).toMinutes();
                    endTime = list.get(i).getEndDate();
                } else { // no overlapped
                    count += Duration.between(list.get(i).getBeginDate(), list.get(i).getEndDate()).toMinutes();
                    endTime = list.get(i).getEndDate();
                }
                log.debug("{} - {}", list.get(i).getId(), count);
            }
        }

        if (!overlappedTimeEntryList.isEmpty()) {
            String overlappedTimeEntry = overlappedTimeEntryList.stream().distinct().map(item -> item.getId() + "").collect(Collectors.joining(", "));
//            String overlappedTimeEntry = overlappedTimeEntryList.stream().distinct().map(item -> suffixTimeEntryLink(item, cwWebSit, mapping.getTz())).collect(Collectors.joining(", "));
            result.put("overlappedTimeEntry", overlappedTimeEntry);
        }
        result.put("timeEntryDuration", count);
        result.put("overlapped", overlapped);
        return result;
    }

    @Override
    public Map<String, Object> getTimeEntryDurationTest(UserMappingResponse mapping) {
        Map<String, Object> result = new HashMap<>();
        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(mapping.getTz()));
        LocalDateTime begin = TimeZoneUtils.zoneToUtc(LocalDateTime.of(zonedDateTime.toLocalDate(), LocalTime.MIN), mapping.getTz()).toLocalDateTime();
        String minTime = begin.format(DateTimeFormatter.ISO_DATE_TIME);
        log.info("mapping:{}", mapping);
        String conditions = "lastUpdated>[" + minTime + "] AND member/identifier='" + mapping.getIdentifier() + "'";
        //"lastUpdated>[2020-06-29T00:00:00Z] AND member/identifier='qiao.rong'
        log.info("conditions:{}", conditions);
        String url = "http://mspbots-wise/times/page?tenantId=" + mapping.getTenantId() + "&conditions=" + conditions;
        url = url + "&&fields=id,timeStart,timeEnd";
        //&fields=id,hoursDeduct,actualHours,_info/lastUpdated,member/identifier
        JsonNode node = this.restTemplate.getForObject(url, JsonNode.class);
        log.info("timeentry tenantId{},identifier:{}, node:{}", mapping.getTenantId(), mapping.getIdentifier(), node);

        long count = 0;
        boolean overlapped = false;
        List<Long> overlappedTimeEntryList = new ArrayList<>();
        if (node.size() > 0) {
            LocalDateTime timeStart = ZonedDateTime.parse(node.get(0).get("timeStart").asText()).toLocalDateTime();
            LocalDateTime timeEnd = ZonedDateTime.parse(node.get(0).get("timeEnd").asText()).toLocalDateTime();
            count = Duration.between(timeStart, timeEnd).toMinutes();
            LocalDateTime endTime = ZonedDateTime.parse(node.get(0).get("timeEnd").asText()).toLocalDateTime();
            for (int i = 1, j = node.size(); i < j; i++) {
                if (ZonedDateTime.parse(node.get(i).get("timeStart").asText()).toLocalDateTime().isBefore(endTime)) {
                    overlapped = true;
                    overlappedTimeEntryList.add(node.get(i - 1).get("id").asLong());
                    overlappedTimeEntryList.add(node.get(i).get("id").asLong());
                    if (!ZonedDateTime.parse(node.get(i).get("timeEnd").asText()).toLocalDateTime().isAfter(endTime)) {
                        continue;
                    }
                    count += Duration.between(ZonedDateTime.parse(node.get(i - 1).get("timeEnd").asText()).toLocalDateTime(), ZonedDateTime.parse(node.get(i).get("timeEnd").asText()).toLocalDateTime()).toMinutes();
                    endTime = ZonedDateTime.parse(node.get(i).get("timeEnd").asText()).toLocalDateTime();
                } else { // no overlapped
                    count += Duration.between(ZonedDateTime.parse(node.get(i).get("timeStart").asText()).toLocalDateTime(), ZonedDateTime.parse(node.get(i).get("timeEnd").asText()).toLocalDateTime()).toMinutes();
                    endTime = ZonedDateTime.parse(node.get(i).get("timeEnd").asText()).toLocalDateTime();
                }

            }
        }

        if (!overlappedTimeEntryList.isEmpty()) {
            String overlappedTimeEntry = overlappedTimeEntryList.stream().distinct().map(item -> item + "").collect(Collectors.joining(", "));
            result.put("overlappedTimeEntry", overlappedTimeEntry);
        }
        result.put("timeEntryDuration", count);
        result.put("overlapped", overlapped);
        return result;
    }

    private Long getAttendanceDuration(UserMappingResponse mapping) {
        Users users = this.usersMapper.selectOne(Wrappers.<Users>lambdaQuery().eq(Users::getUsername, mapping.getTeamsEmail().toLowerCase()));
        if (ObjectUtils.isEmpty(users)) {
            return 480L;
        } else {
            ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(mapping.getTz()));
            LocalDateTime begin = TimeZoneUtils.zoneToUtc(LocalDateTime.of(zonedDateTime.toLocalDate(), LocalTime.MIN), mapping.getTz()).toLocalDateTime();
            LocalDateTime end = TimeZoneUtils.zoneToUtc(LocalDateTime.of(zonedDateTime.toLocalDate(), LocalTime.MAX), mapping.getTz()).toLocalDateTime();


            TenantAttendance param = new TenantAttendance();
            param.setTenantId(mapping.getTenantId());
            param.setUserId(users.getId());
            param.setStartTime(begin);
            param.setEndTime(end);


            List<TenantAttendance> attendances = this.baseMapper.selectUserAttendances(param);
            AtomicReference<Long> count = new AtomicReference<>(0L);
            attendances.stream().forEach(t -> {
                if (ObjectUtils.isEmpty(t.getDuration())) {
                    count.updateAndGet(v -> v + Duration.between(t.getLogTime(), LocalDateTime.now()).toMinutes());
                } else {
                    count.updateAndGet(v -> v + t.getDuration());
                }
            });

            if (count.get() == 0) {
                return 480L;
            } else {
                return count.get();
            }

        }
    }

    private Long getAttendanceDuration(UserMappingResponse mapping, String time) {
        AttendancesController.AttendanceParams queryLogParams = AttendancesController.AttendanceParams.builder().build();
        queryLogParams.setTenantId(mapping.getTenantId());
        queryLogParams.setTeamsId(mapping.getTeamsUserId());
        queryLogParams.setUserId(mapping.getTenantUserId());
        LocalDateTime startTime = LocalDateTime.now(ZoneOffset.of(mapping.getTz()));
        queryLogParams.setStartTime(startTime.minusHours(12));
        queryLogParams.setEndTime(startTime.plusMinutes(1));
        queryLogParams.setType(AttendanceTypeEnum.IN);
        Attendance attendance = this.attendancesService.findLogLast(queryLogParams);
        if (attendance != null) {
            QueryWrapper<Attendance> queryWrapper = new QueryWrapper<>();
            queryWrapper.ge("log_time", attendance.getLogTime());
            queryWrapper.eq("tenant_user_id", attendance.getTenantUserId());
            queryWrapper.eq("tenant_id", attendance.getTenantId());
            queryWrapper.eq("teams_user_id", attendance.getTeamsUserId());
            queryWrapper.orderByAsc("log_time");
            List<Attendance> list = this.attendancesService.list(queryWrapper);
            LocalDateTime outTime = LocalDateTime.now(ZoneOffset.of(mapping.getTz()));
            if (!StringUtils.isEmpty(time)) {
                outTime = LocalDateTime.of(LocalDate.now(ZoneId.of(mapping.getTz())), LocalTime.parse(time));
            }
            Long total = Duration.between(attendance.getLogTime(), outTime).toMinutes();
            for (int i = 1, j = list.size(); i < j; i++) {
                if (list.get(i).getType() == AttendanceTypeEnum.BACK_FROM_BREAK
                        || list.get(i).getType() == AttendanceTypeEnum.BACK_FROM_LUNCH) {

                    total -= Duration.between(list.get(i - 1).getLogTime(), list.get(i).getLogTime()).toMinutes();
                }
            }
            return total.intValue() == 0 ? 1 : total;
        } else {
            return 480L;
        }

    }

    private StringBuilder suffixTicketLink(Long tenantId, Long ticketId) {
        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append(
                "[<a href='https://" + getConfiguration(tenantId, KEYWORD_WISE_SITE) + "/v4_6_release/ConnectWise.aspx?routeTo=ServiceFV&recid=");
        messageBuilder.append(ticketId).append("'>");
        messageBuilder.append(ticketId).append("</a>]&nbsp;");
        return messageBuilder;
    }

    private StringBuilder suffixTimeEntryLink(TenantTimeEntry timeEntry, String webSite, String tzStr) {
        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append(
                "<a href='https://" + webSite + "/v4_6_release/ConnectWise.aspx?routeTo=TimeEntryFV&recid=");
        messageBuilder.append(timeEntry.getCwTid()).append("'>");
        messageBuilder.append(timeEntry.getCwTid()).append("</a> ")
                .append("(").append(TimeZoneUtils.utcToZone(timeEntry.getBeginDate(), tzStr).format(DateTimeFormatter.ofPattern("HH:mm")))
                .append(" - ")
                .append(TimeZoneUtils.utcToZone(timeEntry.getEndDate(), tzStr).format(DateTimeFormatter.ofPattern("HH:mm")))
                .append(")").append("&nbsp;");
        return messageBuilder;
    }

    private StringBuilder suffixCalendarLink(Long tenantId) {
        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append(
                "[<a href='https://" + getConfiguration(tenantId, KEYWORD_WISE_SITE) + "/v4_6_release/ConnectWise.aspx?#startscreen=my_calendar");
        messageBuilder.append("'>");
        messageBuilder.append("Calendar").append("</a>]&nbsp;");
        return messageBuilder;
    }

    protected String getConfiguration(Long tenantId, String keyword) {
        TenantConfiguration tenantConfiguration = tenantConfigurationService.selectByKeyword(tenantId, keyword);
        return ObjectUtils.isEmpty(tenantConfiguration) ? "" : tenantConfiguration.getKeywordValue();
    }


}
